$msg = @"
      ____                          _____ __         ____
     / __ \____ _      _____  _____/ ___// /_  ___  / / /
    / /_/ / __ \ | /| / / _ \/ ___/\__ \/ __ \/ _ \/ / /
   / ____/ /_/ / |/ |/ /  __/ /   ___/ / / / /  __/ / /
  /_/    \____/|__/|__/\___/_/   /____/_/ /_/\___/_/_/

"@

function Hello {
  param (
    [string] $Msg
  )

  Write-Host $Msg -ForegroundColor DarkBlue
}

$(Hello $msg)
Remove-Variable -Name "msg"

# On MacOS, $platform is Unix
# On Win10, $platform is Win32NT
# $platform = [System.Environment]::OSVersion.Platform

# ======================= Import Modules =======================
Import-Module PSReadLine
# NOTE: ZLocation is imported at last
Import-Module ZLocation

# Auto complete
Set-PSReadlineOption -EditMode Emacs
Set-PSReadlineOption -PredictionSource History
Set-PSReadlineKeyHandler -Key Tab -Function MenuComplete

Set-PSReadlineOption -Color @{
  Command = "DarkGreen"
  Comment = "Gray"
  Error = "DarkRed"
  Number = "White"
  String = "White"
  Keyword = "DarkYellow"
  Default = "DarkGray"
  Operator = "Gray"
  Type = "DarkBlue"
}

# Auto complete commands from history
Set-PSReadlineKeyHandler -Key UpArrow -Function HistorySearchBackward
Set-PSReadlineKeyHandler -Key DownArrow -Function HistorySearchForward
Set-PSReadlineKeyHandler -Key "Ctrl+z" -Function Undo

function GitCurrentBranch {
  $branch = $(git rev-parse --abbrev-ref HEAD) 2>$null
  if ($branch -eq "" -or $null -eq $branch) {
    return $null
  }

  return $branch
}

function IsGitRepo {
  $branch = $GitCurrentBranch
  return $null -eq $branch
}

function GitStashCount {
  param (
    [string[]] $Lines
  )

  $stash = $Lines | Where-Object -FilterScript { $_.StartsWith("# stash")}
  if ($null -eq $stash -or "" -eq $stash) {
    return 0
  }

  $tmp = $stash -split " "
  return [int] $tmp[2]
}

function GitTagByCommit {
  param (
    [string] $CommitId
  )

  $tag = $(git tag --points-at $CommitId) 2> $null
  if ($tag -eq "" -or $null -eq $tag) {
    $tag = ""
  } else { # 🚩 § ⚡ 🏷
    # $tag = "  {0}" -f $tag
    $tag = " 🚩{0}" -f $tag
  }
  return $tag
}

function GitOid {
  param (
    [string[]]$Lines
  )

  $branchOid = $Lines | Where-Object -FilterScript { $_.StartsWith("# branch.oid") }
  if ($null -eq $branchOid -or $branchOid -eq "") {
    return ""
  }

  $tmp= $branchOid -split " "
  $longCommitId = $tmp[2]
  $commitId = $longCommitId.Substring(0, 7)
  return $commitId
}

function GitBranchInfo {
  param (
    [string[]] $Lines
  )

  $CommitId = $(GitOid $Lines)
  $branchInfo = $Lines | Where-Object -FilterScript { $_.StartsWith("# branch.head") }
  $tmp = $branchInfo -split " "
  $branchName = $tmp[2]
  $tag = $(GitTagByCommit $CommitId)
  if ($branchName -eq "(detached)") {
    $branchName = "{0}{1}" -f $CommitId, $tag
  } else {
    $branchName = "{0}{1}" -f $branchName, $tag
  }

  return $branchName
}

function GitUpstream {
  param (
    [string[]]$Lines
  )

  $upstream = ""
  $upstreamInfo = $lines | Where-Object -FilterScript { $_.StartsWith("# branch.upstream") }
  if ($null -ne $upstreamInfo -and $upstreamInfo -ne "") {
    $tmp = $upstreamInfo -split " "
    $upstream = $tmp[2]
  }

  return $upstream
}

function GitDiff {
  param (
    [string[]]$Lines
  )

  $diffInfo = $lines | Where-Object -FilterScript { $_.StartsWith("# branch.ab") }
  $ahead = 0
  $behind = 0
  if ($null -ne $diffInfo -and $diffInfo -ne "") {
    $tmp = $diffInfo -split " "
    $ahead = [int]$tmp[2]
    $behind = [System.Math]::Abs([int]$tmp[3])
  }

  return $ahead, $behind
}

function GitChange {
  param (
    [string[]]$Lines
  )

  $stagingList = $Lines | Where-Object -FilterScript { $_.StartsWith("1") -or $_.StartsWith("2") }
  $totalChanged = 0
  $added = 0
  $modified = 0
  $deleted = 0
  $unstaged = 0
  $staged = 0
  if ($null -ne $stagingList) {
    $totalChanged = $stagingList.Count
    foreach ($line in $stagingList) {
      $tmp = $line -split " "
      $flag = $tmp[1]
      if ($flag.Contains("A")) {
        $added++
      } elseif ($flag.Contains("M") -or $flag.Contains("R")) {
        $modified++
      } elseif ($flag.Contains("D")) {
        $deleted++
      }

      if ("." -eq $flag[0]) {
        $unstaged++
      }

      if ("." -eq $flag[1]) {
        $staged++
      }
    }
  }

  return $added, $modified, $deleted, $totalChanged, $unstaged, $staged
}

function GitUntracked {
  param (
    [string[]]$Lines
  )

  $untrackedList = $lines | Where-Object -FilterScript { $_.StartsWith("?") }
  $untrackedNum = 0
  if ($null -ne $untrackedList) {
    $untrackedNum = $untrackedList.Count
  }

  return $untrackedNum
}

function GitConflict {
  param (
    [string[]] $Lines
  )

  $conflicts = $Lines | Where-Object -FilterScript { $_.StartsWith("u") }
  $conflictNum = 0
  if ($null -ne $conflicts) {
    $conflictNum = $conflicts.Count
  }

  return $conflictNum
}

function GetGitInfo {
  $lines = $(git --no-optional-locks status --porcelain=2 --branch --show-stash) 2> $null
  if ($lines -eq "" -or $null -eq $lines) {
    return $null
  }

  # output example
  # branch.oid eef0ee61f64a0ccecafa75ca338f8e958725ce95
  # branch.head master
  # branch.upstream origin/master
  # branch.ab +0 -0
  $branchName = $(GitBranchInfo $lines)
  $upstream = $(GitUpstream $lines)
  $ahead, $behind = $(GitDiff $lines)
  $added, $modified, $deleted, $totalChanged, $unstaged, $staged = $(GitChange $lines)
  $untrackedNum = $(GitUntracked $lines)
  $conflictNum = $(GitConflict $lines)
  $stashed = $(GitStashCount $lines)

  $info = @{
    BranchName = $branchName
    Upstream = $upstream
    Ahead = $ahead
    Behind = $behind
    Added = $added
    Modified = $modified
    Deleted = $deleted
    TotalChanged = $totalChanged
    Unstaged = $unstaged
    Staged = $staged
    ConflictNum = $conflictNum
    Stashed = $stashed
    UntrackedNum = $untrackedNum
  }
  return $info
}

function WriteGitInfo {
  if (!$(IsGitRepo)) {
    return
  }

  # git prompt
  $gitInfo = $(GetGitInfo)
  # Write-Host $gitInfo -NoNewline
  if ($null -eq $gitInfo) {
    return
  }

  Write-Host "(" -ForegroundColor DarkGreen -NoNewline
  $branchName = "{0}" -f $gitInfo.BranchName
  Write-Host $branchName -ForegroundColor DarkBlue -NoNewline
  if ($gitInfo.Ahead -gt 0) {
    $tmp = "↑{0}" -f $gitInfo.Ahead
    Write-Host $tmp -ForegroundColor DarkGreen -NoNewline
  }

  if ($gitInfo.Behind -gt 0) {
    $tmp = "↓{0}" -f $gitInfo.Behind
    Write-Host $tmp -ForegroundColor DarkRed -NoNewline
  }

  if ($gitInfo.Stashed -gt 0) {
    $tmp = "▣{0}" -f $gitInfo.Stashed
    Write-Host $tmp -ForegroundColor DarkCyan -NoNewline
  }

  if ($gitInfo.UntrackedNum -gt 0) {
    $tmp = "?{0}" -f $gitInfo.UntrackedNum
    Write-Host $tmp -ForegroundColor Magenta -NoNewline
  }

  if ($gitInfo.Added -gt 0 -or
      $gitInfo.Modified -gt 0 -or
      $gitInfo.Deleted -gt 0 -or
      $gitInfo.TotalChanged -gt 0) {
    Write-Host "|" -ForegroundColor White -NoNewline
  }

  if ($gitInfo.TotalChanged -gt 0) {
    $tmp = "±{0}:" -f $gitInfo.TotalChanged # ⁕
    Write-Host $tmp -ForegroundColor DarkMagenta -NoNewline
  }

  if ($gitInfo.Added -gt 0) {
    $tmp = "+{0}" -f $gitInfo.Added
    Write-Host $tmp -ForegroundColor DarkGreen -NoNewline
  }

  if ($gitInfo.Modified -gt 0) {
    $tmp = "~{0}" -f $gitInfo.Modified
    Write-Host $tmp -ForegroundColor DarkYellow -NoNewline
  }

  if ($gitInfo.Deleted -gt 0) {
    $tmp = "-{0}" -f $gitInfo.Deleted
    Write-Host $tmp -ForegroundColor DarkRed -NoNewline
  }

  if ($gitInfo.Unstaged -gt 0 -or
      $gitInfo.Staged -gt 0) {
    Write-Host "|" -ForegroundColor White -NoNewline
  }
  if ($gitInfo.Unstaged -gt 0) {
    $tmp = "✗{0}" -f $gitInfo.Unstaged
    Write-Host $tmp -ForegroundColor DarkRed -NoNewline
  }

  if ($gitInfo.Staged -gt 0) {
    $tmp = "✓{0}" -f $gitInfo.Staged
    Write-Host $tmp -ForegroundColor DarkGreen -NoNewline
  }

  if ($gitInfo.ConflictNum -gt 0) {
    $tmp = "|merge:{0}" -f $gitInfo.ConflictNum
    Write-Host $tmp -ForegroundColor Red -NoNewline
  }

  Write-Host ") " -ForegroundColor DarkGreen -NoNewline
}

function Prompt {
  $arrow = "❯❯❯ "
  Write-Host "# " -ForegroundColor White -NoNewline
  # $lastStatus = $?
  if ($IsWindows) { # Besides, $IsLinux and $IsMacOS are available
    $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
    $principal = [Security.Principal.WindowsPrincipal] $identity
    $adminRole = [Security.Principal.WindowsBuiltInRole]::Administrator
    if($principal.IsInRole($adminRole)) {
      Write-Host "[ADMIN!] " -ForegroundColor DarkRed -NoNewline
    }
  }

  # Write time info
  Write-Host "[$(Get-Date -Format "HH:mm:ss")] " -ForegroundColor DarkMagenta -NoNewline
  # Write current directory
  $cwd = $(Get-Location).ToString()
  if ($cwd.StartsWith($Home)) {
    $reg = "^({0})?" -f $Home
    $reg = $reg.Replace("\", "\\")
    $cwd = $cwd -replace $reg, "~"
  }
  Write-Host "$cwd " -ForegroundColor DarkCyan -NoNewline

  $(WriteGitInfo)

  # Write-Host "❯❯❯" -ForegroundColor DarkCyan -NoNewline
  # if ($lastStatus) {
  #   Write-Host "❯❯❯" -ForegroundColor DarkCyan -NoNewline
  # } else {
  #   Write-Host "❯❯❯" -ForegroundColor DarkRed -NoNewline
  # }
  return "$arrow"
}

# Define aliases or function similar to *nix
Set-Alias type Get-Command
Set-Alias pbcopy Set-Clipboard
Set-Alias pbpaste Get-Clipboard

function man {
  Get-Help -Detailed $args
}

# Remove conflict aliases
Remove-Alias gc -Force -ErrorAction SilentlyContinue # Get-Content
Remove-Alias gl -Force -ErrorAction SilentlyContinue # Get-Location
Remove-Alias gp -Force -ErrorAction SilentlyContinue # Get-Property
Remove-Alias gcb -Force -ErrorAction SilentlyContinue
Remove-Alias gcm -Force -ErrorAction SilentlyContinue
# Remove-Alias gcs
# Remove-Alias gm
# Remove-Alias gpv

# Define git aliases referring to git plugin of oh-my-zsh.
function g {
  git $args
}

function ga {
  git add $args
}

function gaa {
  git add --all $args
}

function gapa {
  git add --patch $args
}

function gau {
  git add --update $args
}

function gb {
  git branch $args
}

function gba {
  git branch -a $args
}

function gbd {
  param (
    [Parameter(Mandatory)]
    [ValidateNotNullOrEmpty()]
    [ArgumentCompleter({
      param($pCmd, $pParam, $pWord, $pAst, $pFakes)
      $MergedBranchs = $(git branch --merged | Select-String "^(\*|\s*(master|main|develop|dev)\s*$)" -NotMatch).Line
      if ([string]::IsNullOrEmpty($pWord)) {
        return $MergedBranchs
      }

      $MergedBranchs | Select-String "$pWord"
    })]
    [string] $branch
  )
  git branch -d $branch
}

function gbdf {
  param (
    [Parameter(Mandatory)]
    [ValidateNotNullOrEmpty()]
    [ArgumentCompleter({
      param($pCmd, $pParam, $pWord, $pAst, $pFakes)
      $MergedBranchs = $(git branch --merged | Select-String "^(\*|\s*(master|main|develop|dev)\s*$)" -NotMatch).Line
      if ([string]::IsNullOrEmpty($pWord)) {
        return $MergedBranchs
      }

      $MergedBranchs | Select-String "$pWord"
    })]
    [string] $branch
  )
  git branch -D $branch
}

function gbda {
  $MergedBranchs = $(git branch --merged | Select-String "^(\*|\s*(master|main|develop|dev)\s*$)" -NotMatch).Line
  $MergedBranchs | ForEach-Object {
    if ([string]::IsNullOrEmpty($_)) {
      return
    }
    git branch -d $_.Trim()
  }
}

function gbl {
  git blame -b -w $args
}

function gbnm {
  git branch --no-merged $args
}

function gbr {
  git branch --remote $args
}

function gbs {
  git bisect $args
}

function gbsb {
  git bisect bad $args
}

function gbsg {
  git bisect good $args
}

function gbsr {
  git bisect reset $args
}

function gbss {
  git bisect start $args
}

function gc {
  git commit -v $args
}

function gc! {
  git commit -v --amend $args
}

function gca {
  git commit -v -a $args
}

function gcam {
  git commit -a -m $args
}

function gca! {
  git commit -v -a --amend $args
}

function gcan! {
  git commit -v -a -s --no-edit --amend $args
}

function gcb {
  git checkout -b $args
}

function gcf {
  git config --list $args
}

function gcl {
  git clone --recursive $args
}

function gclean {
  git clean -df $args
}

function gcm {
  git checkout master $args
}

function gcd {
  git checkout develop $args
}

function gcmsg {
  git commit -m $args
}

function gco {
  # git checkout $args
  param(
    [Parameter(Mandatory)]
    [ValidateNotNullOrEmpty()]
    [ArgumentCompleter({
      param($pCmd, $pParam, $pWord, $pAst, $pFakes)
      $branchList = $(git branch --format='%(refname:short)') 2> $null
      $tagList = $(git tag) 2> $null
      $items = @( $branchList ) + @( $tagList )
      if ([string]::IsNullOrWhiteSpace($pWord)) {
        return $items
      }

      $items | Select-String "$pWord"
    })]
    [string] $branch
  )

  git checkout $branch;
}

function gcount {
  git shortlog -sn $args
}

function gcp {
  git cherry-pick $args
}

function gcpa {
  git cherry-pick --abort $args
}

function gcpc {
  git cherry-pick --continue $args
}

function gcs {
  git commit -S $args
}

function gd {
  git diff $args
}

function gdca {
  git diff --cached $args
}

function gdt {
  git diff-tree --no-commit-id --name-only -r $args
}

function gdw {
  git diff --word-diff $args
}
function gf {
  git fetch $args
}

function gfa {
  git fetch --all --prune $args
}

function gfo {
  git fetch origin $args
}

function gg {
  git gui citool $args
}

function gga {
  git gui citool --amend $args
}

function ggf {
  $CurrentBranch = $(GitCurrentBranch)

  git push --force origin $CurrentBranch
}

function ggfl {
  $CurrentBranch = $(GitCurrentBranch)
  git push --force-with-lease origin $CurrentBranch
}

function ghh {
  git help $args
}

function ggsup {
  $CurrentBranch = $(GitCurrentBranch)
  git branch --set-upstream-to=origin/$CurrentBranch
}

function gpsup {
  $CurrentBranch = $(GitCurrentBranch)
  git push --set-upstream origin $CurrentBranch
}

function gignore {
  git update-index --assume-unchanged $args
}

function gignored {
  git ls-files -v | Select-String "^[a-z]" -CaseSensitive
}

function glg {
  git log --stat --color $args
}

function glgg {
  git log --graph --color $args
}

function glgga {
  git log --graph --decorate --all $args
}

function glgm {
  git log --graph --max-count=10 $args
}

function glgp {
  git log --stat --color -p $args
}

function glo {
  git log --oneline --decorate --color $args
}

function glog {
  git log --oneline --decorate --color --graph $args
}

function glol {
  git log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit $args
}

function glola {
  git log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit --all $args
}

function gmerge {
  git merge $args
}

function gmom {
  git merge origin/master $args
}

function gmt {
  git mergetool --no-prompt $args
}

function gmtvim {
  git mergetool --no-prompt --tool=vimdiff $args
}

function gmum {
  git merge upstream/master $args
}

function gpd {
  git push --dry-run $args
}

function gpf {
  git push --force-with-lease $args
}

function gpf! {
  git push --force $args
}

function gpoat {
  git push origin --all
  git push origin --tags
}

function gpristine {
  git reset --hard
  git clean -dfx
}

function gpu {
  git push upstream $args
}

function gpull {
  git pull $args
}

function gpush {
  git push $args
}

function gpushv {
  git push -v $args
}

function gr {
  git remote $args
}

function gra {
  git remote add $args
}

function grb {
  git rebase $args
}

function grba {
  git rebase --abort $args
}

function grbc {
  git rebase --continue $args
}

function grbi {
  git rebase -i $args
}

function grbm {
  git rebase master $args
}

function grbs {
  git rebase --skip $args
}

function grh {
  git reset $args
}

function grhh {
  git reset --hard $args
}

function grmv {
  git remote rename $args
}

function grrm {
  git remote remove $args
}

function grset {
  git remote set-url $args
}

function grt {
  try {
    $RootPath = git rev-parse --show-toplevel
  }
  catch {
    $RootPath = "."
  }
  Set-Location $RootPath
}

function gru {
  git reset -- $args
}

function grup {
  git remote update $args
}

function grv {
  git remote -v $args
}

function gsb {
  git status -sb $args
}

function gsd {
  git svn dcommit $args
}

function gsh {
  git show $args
}

function gsi {
  git submodule init $args
}

function gsps {
  git show --pretty=short --show-signature $args
}

function gsr {
  git svn rebase $args
}

function gss {
  git status -s $args
}

function gst {
  git status $args
}
function gsta {
  git stash save $args
}

function gstaa {
  git stash apply $args
}

function gstd {
  git stash drop $args
}

function gstl {
  git stash list $args
}

function gstp {
  git stash pop $args
}

function gstc {
  git stash clear $args
}

function gsts {
  git stash show --text $args
}

function gsu {
  git submodule update $args
}

function gts {
  git tag -s $args
}

function gunignore {
  git update-index --no-assume-unchanged $args
}

function gunwip {
  Write-Output $(git log -n 1 | Select-String "--wip--" -Quiet).Count
  git reset HEAD~1
}

function gup {
  git pull --rebase $args
}

function gupv {
  git pull --rebase -v $args
}

function glum {
  git pull upstream master $args
}

function gvt {
  git verify-tag $args
}

function gwch {
  git whatchanged -p --abbrev-commit --pretty=medium $args
}

function gwip {
  git add -A
  git rm $(git ls-files --deleted) 2> $null
  git commit --no-verify -m "--wip-- [skip ci]"
}

function ggl {
  $CurrentBranch = $(GitCurrentBranch)
  git pull origin $CurrentBranch
}

function ggpush {
  $CurrentBranch = $(GitCurrentBranch)
  git push origin $CurrentBranch
}


# Useful custom functions
function Convert-Files($path, $source, $dest) {
  Get-ChildItem -File -Recurse $path | ForEach-Object {
    Convert-File $_.FullName $source $dest
  }
}

function Convert-File($file, $source, $dest) {
  $utf8NoBomEncoding = New-Object System.Text.UTF8Encoding($False)
  Write-Host -ForegroundColor Darkgreen "Conversion finished: ", $file
  $x =[System.IO.File]::ReadAllText($file)
  $content = $x -replace $source, $dest
  [System.IO.File]::WriteAllText($file, $content, $utf8NoBomEncoding)
}

function dos2unix($pattern) {
  Convert-Files $pattern "`r`n" "`n"
}

function unix2dos($pattern) {
  Convert-Files $pattern "`n" "`r`n"
}

function open($path) {
  if ($path) {
    Invoke-Item $path
  } else {
    Invoke-Item "."
  }
}

function timestamp {
  $timestamp = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
  Write-Host $timestamp
}

function which {
  param (
    [Parameter(Mandatory)]
    [string] $Cmd
  )

  $cmdInfo = $(Get-Command $Cmd)
  if ($null -eq $cmdInfo) {
    return
  }

  Write-Output $cmdInfo.Source
}

<#
.SYNOPSIS
  Returns AES key by specified length.
.PARAMETER Length
  Length of AES key, normally is 16, 24, 32, 64
.EXAMPLE
  NewAESKey 16
.INPUTS
  int32
.OUTPUTS
  String
#>
function NewAESKey {
  param (
    [Parameter(Mandatory)]
    [ValidateSet(16, 24, 32, 64)]
    [int32] $Length
  )

  $alpahBet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"

  $key = [System.Text.StringBuilder]::New($Length, $Length)
  $indexes = Get-Random -Maximum $alpahBet.Length -Count $Length
  for ([int] $i = 0; $i -lt $Length; $i++) {
    $index = $indexes[$i]
    [void]$key.Append($alpahBet[$index])
  }

  return $key.ToString()
}

function hash {
  param (
    [Parameter(Mandatory, ValueFromPipeline)]
    [ValidateNotNullOrEmpty()]
    $Content
  )

  $stringAsStream = [System.IO.MemoryStream]::new()
  $writer = [System.IO.StreamWriter]::new($stringAsStream)
  $writer.write($Content)
  $writer.Flush()
  $stringAsStream.Position = 0

  $md5 = $(Get-FileHash -InputStream $stringAsStream -Algorithm MD5)
  $sha1 = $(Get-FileHash -InputStream $stringAsStream -Algorithm SHA1)
  $sha256 = $(Get-FileHash -InputStream $stringAsStream -Algorithm SHA256)
  $sha384 = $(Get-FileHash -InputStream $stringAsStream -Algorithm SHA384)
  $sha512 = $(Get-FileHash -InputStream $stringAsStream -Algorithm SHA512)

  return @{
    MD5 = $md5.Hash;
    SHA1 = $sha1.Hash;
    SHA256 = $sha256.Hash;
    SHA384 = $sha384.Hash;
    SHA512 = $sha512.Hash;
  }
}
